package simel;

import java.util.*;
import java.io.*;

import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import javax.swing.*;

public class Simulator
{
    ArrayList<Elevator> elevators;
    Agent agent;
    ArrayList<Passenger> passengers = new ArrayList<Passenger>();
    int time;

    double totalPassengerTime = 0, totalPassengerTimeSq = 0;
    int    totalPassengers = 0;

    int    floorMax, floorMin;

    JFrame jf;
    Widget widget;

    Random r;

    int simulationDelayMS; // how long to delay between frames?

    double totalPassengerRate;
    double passengerRates[];    // weights that determine where agents appear. Sums to totalPassengerRate.

    /** @param totalPassengerRate average number of passengers that appear per time step
        @param passengerRateVariability how different are the different floors?
    **/
    public Simulator(ArrayList<Elevator> elevators, Agent agent, int seed, double totalPassengerRate, double passengerRateVariability, boolean gui)
    {
        this.elevators = elevators;
        this.agent = agent;
        this.r = new Random(seed);
        this.totalPassengerRate = totalPassengerRate;

        floorMin = Integer.MAX_VALUE;
        floorMax = Integer.MIN_VALUE;
        for (Elevator e : elevators) {
            floorMin = Math.min(floorMin, e.getFloorMin());
            floorMax = Math.max(floorMax, e.getFloorMax());
        }

        passengerRates = new double[floorMax - floorMin + 1];

        // variability of zero: uniform.
        double tmp = 0;
        for (int i = 0; i < passengerRates.length; i++) {
            passengerRates[i] = Math.exp(r.nextDouble()*passengerRateVariability);
            tmp += passengerRates[i];
        }

        // normalize sum to 1.0
        for (int i = 0; i < passengerRates.length; i++) {
            passengerRates[i] *= totalPassengerRate / tmp;
//            System.out.printf("%d %f\n", i, passengerRates[i]);
        }

        if (gui) {
            jf = new JFrame("SimElevator");
            jf.setLayout(new BorderLayout());
            widget = new Widget();
            jf.add(widget, BorderLayout.CENTER);
            jf.setSize(600,600);
            jf.setVisible(true);
        }
    }

    class Widget extends JComponent
    {
        public void paint(Graphics _g)
        {
            synchronized(Simulator.this) {
                Graphics2D g = (Graphics2D) _g;
                int width = getWidth(), height = getHeight();

                g.setColor(Color.black);
                g.fillRect(0,0, width, height);

                int floorSpacing = height / (3 + floorMax - floorMin);

                g.translate(0, height);
                g.scale(1, -1);
                g.scale(1, floorSpacing);

                g.setStroke(new BasicStroke(0.05f));
                g.setFont(new Font("SansSerif", Font.PLAIN, 1).deriveFont(Font.PLAIN, AffineTransform.getScaleInstance(floorSpacing, -1)));

                // draw the ground.
                g.setColor(new Color(0, 128, 0));
                g.fillRect(0, floorMin-1, width, floorMin);
                g.setColor(Color.white);
                g.drawString(String.format("t=%d", time), 10, 0);

                int elevatorSpacing = width/2/elevators.size();

                // passengers on each floor
                ArrayList waitingPassengers[] = new ArrayList[floorMax - floorMin + 1];
                for (int i = 0; i < waitingPassengers.length; i++)
                    waitingPassengers[i] = new ArrayList<Passenger>();

                // passengers on each elevator
                ArrayList elevatorPassengers[] = new ArrayList[elevators.size()];
                for (int i = 0; i < elevatorPassengers.length; i++)
                    elevatorPassengers[i] = new ArrayList<Passenger>();

                for (Passenger p : passengers) {
                    if (p.elevator < 0)
                        waitingPassengers[p.getFloor() - floorMin].add(p);
                    else
                        elevatorPassengers[p.elevator].add(p);
                }

                double passengerSz = 0.6;

                for (int f = floorMin; f <= floorMax + 1; f++) {
                    g.setColor(Color.gray);
                    g.drawLine(0, f, width, f);
                    if (f <= floorMax) {
                        g.drawString(""+f, 10, f);

                        for (int i = 0; i < waitingPassengers[f-floorMin].size(); i++) {
                            Passenger p = (Passenger) waitingPassengers[f-floorMin].get(i);
                            Ellipse2D ellipse = new Ellipse2D.Double(0.4*width - i * floorSpacing*passengerSz, f, floorSpacing*passengerSz, passengerSz);
                            g.setColor(randomColor(p.getID()));
                            g.fill(ellipse);
                        }
                    }
                }

                for (int eidx = 0; eidx < elevators.size(); eidx++) {
                    int x0 = width/2 + eidx*elevatorSpacing;
                    int x1 = (int) (x0 + 0.9*(elevatorSpacing));
                    Elevator e = elevators.get(eidx);
                    RoundRectangle2D shaft = new RoundRectangle2D.Double(x0, e.getFloorMin(), 0.9*elevatorSpacing, (e.getFloorMax() - e.getFloorMin())+1, .5, .5);
                    RoundRectangle2D car = new RoundRectangle2D.Double(x0, e.getFloor(), 0.9*elevatorSpacing, 1, .5, .5);
                    g.setColor(Color.gray);
                    g.fill(shaft);
                    g.setColor(Color.darkGray);
                    g.fill(car);


                    for (int i = 0; i < elevatorPassengers[eidx].size(); i++) {
                        Passenger p = (Passenger) elevatorPassengers[eidx].get(i);

                        Ellipse2D ellipse = new Ellipse2D.Double(x0 + (x1 - x0)*i/elevatorPassengers[eidx].size(),
                                                                 e.getFloor(), floorSpacing*passengerSz, passengerSz);
                        g.setColor(randomColor(p.getID()));
                        g.fill(ellipse);
                    }
                }
            }
        }
    }

    public static Color randomColor(long seed)
    {
        Random r = new Random(seed);
        int min = 64;
        return new Color(min+r.nextInt(255-min), min+r.nextInt(255-min), min+r.nextInt(255-min));
    }

    public ArrayList<Elevator> getElevators()
    {
        return elevators;
    }

    public void run(int nsteps)
    {
        while (nsteps > 0) {

            nsteps--;

            synchronized(this) {
                time ++;

                // 1. Create new passengers
                // At most one new passenger per time step per floor.
                for (int f = floorMin; f <= floorMax; f++) {

                    if (r.nextDouble() < passengerRates[f - floorMin]) {
                        int src = f;
                        int dest = floorMin;

                        // draw a random sample from the floors
                        double v = totalPassengerRate * r.nextDouble();
                        for (int i = 0; i < passengerRates.length; i++) {
                            v -= passengerRates[i];
                            if (v < 0) {
                                dest = i + floorMin;
                                break;
                            }
                        }

                        if (src != dest) {
                            Passenger p = new Passenger(time, src, dest);
                            passengers.add(p);
                        }
                    }
                }

                // 2. Let the agent decide what to do.
                agent.update(this, passengers, elevators);

                // 3. Assign passengers to elevators that are headed in their direction. (Auto-boarding)
                for (int pidx = 0; pidx < passengers.size(); pidx++) {
                    Passenger p = passengers.get(pidx);

                    // default value if we don't find a good elevator.
                    p.elevator = -1;

                    for (int eidx = 0; eidx < elevators.size(); eidx++) {
                        Elevator e = elevators.get(eidx);

                        // if the elevator is currently on the right floor, the passenger *could* board it. Should they?
                        if (e.getFloor() == p.getFloor()) {
                            // if the elevator and passenger want to go in the same direction, board!
                            if ((e.getDestination() > e.getFloor() && p.getDestination() > p.getFloor()) ||
                                e.getDestination() < e.getFloor() && p.getDestination() < p.getFloor()) {
                                p.elevator = eidx;
                                break;
                            }
                        }
                    }
                }

                // show passengers getting on elevator.
                if (widget != null)
                    widget.repaint();

                // 4. Move the elevators in accordance with their commands.
                for (Elevator e : elevators) {
                    e.update();
                }

                // 5. Move passengers accordingly
                for (int pidx = 0; pidx < passengers.size(); pidx++) {
                    Passenger p = passengers.get(pidx);

                    if (p.elevator >= 0)
                        p.floor = elevators.get(p.elevator).getFloor();
                }

                // 5. Handle passengers who have arrived at their destination.
                ArrayList<Passenger> activePassengers = new ArrayList<Passenger>();

                for (int pidx = 0; pidx < passengers.size(); pidx++) {
                    Passenger p = passengers.get(pidx);
                    if (p.getFloor() == p.getDestination()) {
                        addPassengerStats(time - p.getTimeBorn());
                    } else
                        activePassengers.add(p);
                }
                passengers = activePassengers;
            }

            if (widget != null)
                widget.repaint();

            if (simulationDelayMS > 0) {
                try {
                    Thread.sleep(simulationDelayMS);
                } catch (InterruptedException ex) {
                }
            }
        }
    }

    void addPassengerStats(double t)
    {
        totalPassengerTime += t;
        totalPassengerTimeSq += t*t;
        totalPassengers++;
    }

    /** Add in statistics for passengers that never reached their destination **/
    public void finish()
    {
        for (int pidx = 0; pidx < passengers.size(); pidx++) {
            Passenger p = passengers.get(pidx);
            addPassengerStats(time - p.getTimeBorn());
        }
    }

    /** What time is it in the simulation? **/
    public int getTime()
    {
        return time;
    }

    // read a line, filtering comments and empty lines
    public static String myReadLine(BufferedReader ins) throws IOException
    {
        while (true) {
            String line = ins.readLine();
            if (line == null)
                return null;

            line = line.trim();
            if (line.startsWith("#"))
                continue;
            if (line.length()==0)
                continue;
            return line;
        }
    }

    public static void main(String args[])
    {
        GetOpt gopt = new GetOpt();

        gopt.addString('a', "agent", "simel.DumbAgent", "Agent class name");
        gopt.addBoolean('h', "help", false, "Show help");
        gopt.addInt('s', "steps", 50000, "Simulation duration (in steps)");
        gopt.addInt('d', "simulation-delay-ms", 50, "How many milliseconds to sleep between animation steps");

        gopt.addBoolean('g', "gui", true, "Enable GUI");

        if (!gopt.parse(args)) {
            System.out.println("Option error: "+gopt.getReason());
            return;
        }

        if (gopt.getBoolean("help")) {
            gopt.doHelp();
            return;
        }

        ////////////////////////////////////////////////////////////
        // Read problem file(s)
        ArrayList<String> extras = gopt.getExtraArgs();
        if (extras.size() == 0) {
            System.out.println("You must specify a building file(s).");
            System.exit(0);
        }

        for (String arg : extras) {

            // read the building file
            ArrayList<Elevator> elevators = new ArrayList<Elevator>();
            double passengerRate = 0;
            double passengerRateVariability = 0;

            try {
                BufferedReader ins = new BufferedReader(new FileReader(arg));

                String tmp = myReadLine(ins);
                if (true) {
                    String toks[] = tmp.split("\\s+");
                    passengerRate = Double.parseDouble(toks[0]);
                    passengerRateVariability = Double.parseDouble(toks[1]);
                }

                tmp = myReadLine(ins);
                int nelevators = Integer.parseInt(tmp);

                for (int i = 0; i < nelevators; i++) {
                    tmp = myReadLine(ins);
                    if (tmp == null)
                        break;
                    String toks[] = tmp.split("\\s+");
                    elevators.add(new Elevator(Integer.parseInt(toks[0]),
                                               Integer.parseInt(toks[1])));
                }

                ins.close();
            } catch (IOException ex) {
                System.out.println("ex: "+ex);
                System.exit(0);
            }

            // set up the simulation
            Agent agent = (Agent) ReflectUtil.createObject(gopt.getString("agent"));

            Simulator sim = new Simulator(elevators, agent, 0, passengerRate, passengerRateVariability, gopt.getBoolean("gui"));
            sim.simulationDelayMS = gopt.getInt("simulation-delay-ms");

            agent.init(sim);
            long startTime = System.currentTimeMillis();
            sim.run(gopt.getInt("steps"));
            sim.finish();
            long endTime = System.currentTimeMillis();

            if (sim.jf != null)
                sim.jf.setVisible(false);

            if (true) {
                int n = sim.totalPassengers;
                double m1 = sim.totalPassengerTime;
                double m2 = sim.totalPassengerTimeSq;

                double mean = m1 / n;
                double stddev = Math.sqrt(m2/n - mean*mean);

                System.out.printf("# passengers   : %d\n", sim.totalPassengers);
                System.out.printf("Average time   : %.3f, stddev: %.3f\n", mean, stddev);
                System.out.printf("Average time^2 : %.3f\n", m2 / n);

                System.out.printf("Wall time      : %.3f s\n", (endTime - startTime) / 1000.0);

                System.out.printf("*** %s %d %f %f %f %f\n\n", arg, sim.totalPassengers, mean, stddev, m2 / n, (endTime - startTime) / 1000.0);
            }
        }

        System.exit(0);
    }


}

